-- Resolve Timekeeper: timer interno para DaVinci Resolve 20.x
-- Auto-instalador: al ejecutarse desde Descargas se copia a Scripts/Comp.
local function installSelf()
    local info = debug and debug.getinfo and debug.getinfo(1, "S") or nil
    local source = info and info.source or ""
    if source:sub(1,1) ~= "@" then return end
    source = source:sub(2)
    local home = os.getenv("HOME")
    if not home or home == "" then return end
    local folder = home .. "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp"
    local destination = folder .. "/Resolve Timekeeper.lua"
    if source == destination then return end
    local input = io.open(source, "rb")
    if not input then return end
    local contents = input:read("*a"); input:close()
    if bmd and bmd.createdir then bmd.createdir(folder) else os.execute("mkdir -p " .. string.format("%q", folder)) end
    local output = io.open(destination, "wb")
    if not output then
        print("Resolve Timekeeper: no se pudo instalar en " .. destination)
        return
    end
    output:write(contents); output:close()
    print("Resolve Timekeeper instalado correctamente.")
    print("Después de reiniciar Resolve aparecerá en Workspace > Scripts > Comp.")
end
installSelf()

local resolve = resolve or bmd.scriptapp("Resolve")
local fu = fu or fusion or resolve:Fusion()
local ui = fu.UIManager
local disp = bmd.UIDispatcher(ui)
local winID = "ResolveTimekeeperWin"

local existing = ui:FindWindow(winID)
if existing then existing:Show(); existing:Raise(); return end

local pages = {"media", "cut", "edit", "fusion", "color", "fairlight", "deliver"}
local labels = {media="Media", cut="Cut", edit="Edit", fusion="Fusion", color="Color", fairlight="Fairlight", deliver="Deliver"}
local dataDir = os.getenv("HOME") .. "/.resolve-timekeeper"
local dataFile = dataDir .. "/segments.tsv"
os.execute("mkdir -p " .. string.format("%q", dataDir))

local function clean(s)
    return tostring(s or ""):gsub("[\t\r\n]", " ")
end

local function currentState()
    local pm = resolve:GetProjectManager()
    local project = pm and pm:GetCurrentProject() or nil
    local page = tostring(resolve:GetCurrentPage() or ""):lower()
    if not project then return nil, "No hay un proyecto abierto" end
    if not labels[page] then return nil, "Página no compatible" end
    local projectName = clean(project:GetName())
    if projectName == "" then projectName = "Proyecto sin nombre" end
    return {id=clean(project:GetUniqueId() or projectName), name=projectName, page=page}, nil
end

local function resolveFrontmost()
    -- El nombre del proceso cambia entre Resolve, Resolve Studio y ventanas UIManager.
    -- El bundle identifier permanece estable y evita esos falsos negativos.
    local p = io.popen([[osascript -e 'tell application "System Events" to get bundle identifier of first application process whose frontmost is true' 2>/dev/null]])
    if not p then return nil end
    local bundle = (p:read("*a") or ""):lower():gsub("%s+", ""); p:close()
    if bundle == "" then return nil end -- sin permiso: no bloquea el conteo
    return bundle:find("blackmagic", 1, true) ~= nil or bundle:find("davinciresolve", 1, true) ~= nil
end

local function idleSeconds()
    local p = io.popen("ioreg -c IOHIDSystem 2>/dev/null | awk '/HIDIdleTime/ {print int($NF/1000000000); exit}'")
    if not p then return nil end
    local value = tonumber(p:read("*a")); p:close(); return value
end

local function appendSegment(state, startedAt, endedAt)
    if not state or endedAt <= startedAt then return end
    local f = io.open(dataFile, "a")
    if f then
        f:write(string.format("%.3f\t%.3f\t%s\t%s\t%s\n", startedAt, endedAt, state.id, state.name, state.page))
        f:close()
    end
end

local function startOf(period)
    local now = os.date("*t")
    now.hour, now.min, now.sec = 0, 0, 0
    local today = os.time(now)
    if period == "week" then return today - ((now.wday + 5) % 7) * 86400 end
    if period == "month" then now.day = 1; return os.time(now) end
    return today
end

local function summary(period, customFrom, customUntil)
    local from, untilNow = customFrom or startOf(period), customUntil or (os.time() + 1)
    local byPage, byProject, total = {}, {}, 0
    for _, page in ipairs(pages) do byPage[page] = 0 end
    local f = io.open(dataFile, "r")
    if f then
        for line in f:lines() do
            local a,b,pid,pname,page = line:match("^([^\t]+)\t([^\t]+)\t([^\t]*)\t([^\t]*)\t([^\t]+)$")
            a, b = tonumber(a), tonumber(b)
            if a and b and labels[page] and b > from and a < untilNow then
                local seconds = math.max(0, math.min(b, untilNow) - math.max(a, from))
                byPage[page] = byPage[page] + seconds
                pname = (pname and pname ~= "") and pname or "Proyecto sin nombre"
                local project = byProject[pid]
                if not project then
                    project = {id=pid, name=pname, seconds=0, last=0, pages={}}
                    for _, p in ipairs(pages) do project.pages[p]=0 end
                    byProject[pid] = project
                end
                project.name = pname
                project.seconds = project.seconds + seconds
                project.last = math.max(project.last, b)
                project.pages[page] = project.pages[page] + seconds
                total = total + seconds
            end
        end
        f:close()
    end
    local projects = {}
    for _, project in pairs(byProject) do table.insert(projects, project) end
    -- "Recientes" significa abiertos/trabajados más recientemente, no los de mayor duración.
    table.sort(projects, function(a,b) return a.last > b.last end)
    return total, byPage, projects
end

local monthNames = {"Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"}
local function availableMonths()
    local grouped = {}
    local f = io.open(dataFile, "r")
    if f then
        for line in f:lines() do
            local a,b = line:match("^([^\t]+)\t([^\t]+)\t")
            a,b=tonumber(a),tonumber(b)
            if a and b then
                local date=os.date("*t",a)
                local key=string.format("%04d-%02d",date.year,date.month)
                local item=grouped[key]
                if not item then
                    local from=os.time{year=date.year,month=date.month,day=1,hour=0,min=0,sec=0}
                    local ny,nm=date.year,date.month+1; if nm==13 then nm,ny=1,ny+1 end
                    item={key=key,label=monthNames[date.month].." "..date.year,from=from,untilAt=os.time{year=ny,month=nm,day=1,hour=0,min=0,sec=0},seconds=0}
                    grouped[key]=item
                end
                item.seconds=item.seconds+math.max(0,b-a)
            end
        end
        f:close()
    end
    local result={}; for _,item in pairs(grouped) do table.insert(result,item) end
    table.sort(result,function(a,b) return a.key>b.key end)
    return result
end

local function totalForProject(projectId)
    if not projectId then return 0 end
    local total = 0
    local f = io.open(dataFile, "r")
    if f then
        for line in f:lines() do
            local a,b,pid = line:match("^([^\t]+)\t([^\t]+)\t([^\t]*)\t")
            a, b = tonumber(a), tonumber(b)
            if a and b and pid == projectId then total = total + math.max(0, b-a) end
        end
        f:close()
    end
    return total
end

local function lifetimeForProject(projectId)
    local total, byPage, byDay = 0, {}, {}
    for _,page in ipairs(pages) do byPage[page]=0 end
    if not projectId then return total,byPage,{} end
    local f=io.open(dataFile,"r")
    if f then
        for line in f:lines() do
            local a,b,pid,pname,page=line:match("^([^\t]+)\t([^\t]+)\t([^\t]*)\t([^\t]*)\t([^\t]+)$")
            a,b=tonumber(a),tonumber(b)
            if a and b and pid==projectId and labels[page] then
                local seconds=math.max(0,b-a)
                total=total+seconds; byPage[page]=byPage[page]+seconds
                local date=os.date("*t",a)
                local key=string.format("%04d-%02d-%02d",date.year,date.month,date.day)
                local day=byDay[key] or {key=key,label=string.format("%02d/%02d/%04d",date.day,date.month,date.year),seconds=0}
                day.seconds=day.seconds+seconds; byDay[key]=day
            end
        end
        f:close()
    end
    local days={}; for _,day in pairs(byDay) do table.insert(days,day) end
    table.sort(days,function(a,b) return a.key>b.key end)
    return total,byPage,days
end

local function resetProject(projectId)
    if not projectId then return false end
    local source = io.open(dataFile, "r")
    if not source then return true end
    local tempPath = dataFile .. ".reset"
    local target = io.open(tempPath, "w")
    if not target then source:close(); return false end
    for line in source:lines() do
        local pid = line:match("^[^\t]+\t[^\t]+\t([^\t]*)\t")
        if pid ~= projectId then target:write(line, "\n") end
    end
    source:close(); target:close()
    os.remove(dataFile .. ".backup")
    os.rename(dataFile, dataFile .. ".backup")
    if os.rename(tempPath, dataFile) then return true end
    os.rename(dataFile .. ".backup", dataFile)
    return false
end

local function clock(value)
    local s = math.max(0, math.floor(value or 0))
    return string.format("%02d:%02d:%02d", math.floor(s/3600), math.floor((s%3600)/60), s%60)
end

local titleFont = ui:Font{Family="Helvetica Neue", PixelSize=21, Bold=true}
local clockFont = ui:Font{Family="Menlo", PixelSize=34, Bold=false, MonoSpaced=true}
local rowTimeFont = ui:Font{Family="Menlo", PixelSize=11, MonoSpaced=true}
local cardStyle = "background:qlineargradient(x1:0,y1:0,x2:1,y2:1,stop:0 #1b1e24,stop:1 #15171c);border:0;border-radius:12px;padding:12px"
local function pageRow(index, page)
    return ui:HGroup{ID="PageRow"..index, Weight=0, Spacing=10,
        ui:Button{ID="PageName"..index, Text=labels[page], MinimumSize={105,25}, Flat=true, Weight=0},
        ui:Label{ID="PageBar"..index, Text="", Alignment={AlignCenter=true}, Weight=1},
        ui:Label{ID="PageTime"..index, Text="00:00:00", Font=rowTimeFont, Alignment={AlignRight=true,AlignVCenter=true}, MinimumSize={90,23}, Weight=0}}
end
local function projectRow(index)
    return ui:HGroup{ID="ProjectRow"..index, Weight=0, Spacing=10, MinimumSize={0,30},
        ui:Label{ID="ProjectName"..index, Text="", MinimumSize={165,23}, Weight=1},
        ui:Label{ID="ProjectBar"..index, Text="", Alignment={AlignCenter=true}, Weight=1},
        ui:Label{ID="ProjectTime"..index, Text="", Font=rowTimeFont, Alignment={AlignRight=true,AlignVCenter=true}, MinimumSize={90,23}, Weight=0}}
end
local win = disp:AddWindow({ID=winID, WindowTitle="Resolve Timekeeper", Geometry={730,100,520,760}, MinimumSize={500,650}},
ui:VGroup{Spacing=10, ContentsMargins={20,16,20,18}, StyleSheet=[[QWidget{background:#101216;color:#f5f3ee;font-size:12px} QLabel{background:transparent} QPushButton{background:#20232a;border:0;border-radius:9px;padding:8px 14px} QPushButton:hover{background:#2a2e37} QPushButton:checked{background:#e7ff58;color:#101216;font-weight:600}]],
    ui:HGroup{Weight=0,
        ui:VGroup{Weight=1, Spacing=0,
            ui:Label{Text="RESOLVE TIMEKEEPER", StyleSheet="color:#8e949f;letter-spacing:2px;font-size:10px;font-weight:700"},
            ui:Label{Text=[[Tu tiempo creativo, <i><span style="color:#e7ff58;font-family:Georgia">bien contado.</span></i>]], Font=titleFont}},
        ui:VGroup{Weight=0, Spacing=4, ui:Button{ID="Pause", Text="Pausar"}, ui:Button{ID="ResetProject", Text="Reiniciar proyecto"}}},
    ui:VGroup{Weight=0, Spacing=3, StyleSheet=cardStyle,
        ui:HGroup{Weight=0,
            ui:Label{ID="Status", Text="● Conectando", StyleSheet="color:#e7ff58;font-weight:700", Weight=1},
            ui:Label{ID="ProjectCount", Text="0 proyectos", Alignment={AlignRight=true}, StyleSheet="color:#7f8590", Weight=0}},
        ui:Label{ID="Clock", Text="00:00:00", Alignment={AlignCenter=true}, Font=clockFont},
        ui:HGroup{Weight=0,
            ui:Label{ID="Project", Text="Buscando proyecto…", StyleSheet="color:#969ca8", Weight=1},
            ui:Label{ID="Total", Text="0 h 00 min", Alignment={AlignRight=true}, StyleSheet="color:#969ca8", Weight=0}}},
    ui:HGroup{Weight=0,
        ui:Button{ID="ProjectPeriod", Text="Proyecto", Checkable=true, Checked=true},
        ui:Button{ID="Day", Text="Hoy", Checkable=true},
        ui:Button{ID="Week", Text="Semana", Checkable=true},
        ui:Button{ID="Month", Text="Mes", Checkable=true},
        },
    ui:VGroup{ID="BreakdownCard", Weight=0, Spacing=2, StyleSheet=cardStyle,
        ui:HGroup{Weight=0,
            ui:Label{ID="BreakdownTitle", Text="TIEMPO POR PÁGINA", StyleSheet="color:#8e949f;letter-spacing:1px;font-size:10px;font-weight:700", Weight=1},
            ui:Button{ID="BackToProjects", Text="‹ Proyectos", Flat=true, Hidden=true, Weight=0}},
        pageRow(1,"media"), pageRow(2,"cut"), pageRow(3,"edit"), pageRow(4,"fusion"),
        pageRow(5,"color"), pageRow(6,"fairlight"), pageRow(7,"deliver")},
    ui:VGroup{ID="RecentCard", Weight=0, Spacing=5, StyleSheet=cardStyle,
        ui:Label{ID="RecentTitle", Text="PROYECTOS RECIENTES", StyleSheet="color:#8e949f;letter-spacing:1px;font-size:10px;font-weight:700"},
        projectRow(1), projectRow(2), projectRow(3),
        ui:HGroup{ID="ProjectRow4", Hidden=true}, ui:HGroup{ID="ProjectRow5", Hidden=true}},
    ui:VGap(3),
    ui:VGroup{Weight=0, Spacing=2,
        ui:Label{Text="LOCAL · PRIVADO · EN TIEMPO REAL", Alignment={AlignCenter=true}, StyleSheet="color:#5f6570;font-size:9px;letter-spacing:1px"},
        ui:Label{Text="@byangelromero", Alignment={AlignCenter=true}, StyleSheet="color:#858b96;font-size:10px;font-weight:600"}}
})

local itm = win:GetItems()
local period, paused, lastState, lastTime, lastCounting = "project", false, nil, os.time(), false
local timer = ui:Timer{ID="ResolveTimekeeperTimer", Interval=250, SingleShot=false}
local ticks, visualTotal, visualProjectTotal, cachedPages, cachedProjects, cachedDays = 0, 0, 0, {}, {}, {}
local resetArmedUntil = 0
local selectedProjectId = nil
local selectedMonth = nil
local currentRowProjects = {}
local currentRowMonths = {}
local lastWindowHeight = 0

local function adjustWindowHeight(recentCount, recentVisible)
    local target = recentVisible and (690 + math.min(3,recentCount) * 30) or 660
    if target ~= lastWindowHeight then
        lastWindowHeight=target
        pcall(function() win:Resize({520,target}) end)
        win:RecalcLayout()
    end
end

local function softBar(ratio)
    local count = math.max(0, math.min(12, math.floor((ratio or 0) * 12 + 0.5)))
    return '<span style="color:#dff55a">' .. string.rep("━", count) ..
           '</span><span style="color:#292d35">' .. string.rep("━", 12-count) .. '</span>'
end

local function refreshPeriodData()
    if period=="project" then
        visualTotal,cachedPages,cachedDays=lifetimeForProject(lastState and lastState.id)
        cachedProjects={}
    elseif period=="month" and selectedMonth then
        visualTotal, cachedPages, cachedProjects=summary("month",selectedMonth.from,selectedMonth.untilAt)
    else
        visualTotal, cachedPages, cachedProjects=summary(period)
    end
end

local function renderBreakdown()
    currentRowProjects = {}
    currentRowMonths = {}
    if period == "project" then
        itm.BreakdownTitle.Text="TIEMPO DEL PROYECTO · HISTÓRICO"
        itm.BackToProjects.Hidden=true; itm.RecentCard.Hidden=false
        itm.RecentTitle.Text="DÍAS DE TRABAJO · RECIENTES"
        local maximum=1; for _,p in ipairs(pages) do maximum=math.max(maximum,cachedPages[p] or 0) end
        for i,p in ipairs(pages) do
            itm["PageRow"..i].Hidden=false; itm["PageName"..i].Text=labels[p]; itm["PageName"..i].Enabled=true
            itm["PageTime"..i].Text=clock(cachedPages[p] or 0); itm["PageBar"..i].Text=softBar((cachedPages[p] or 0)/maximum)
        end
        return
    end
    if period == "day" then
        itm.BreakdownTitle.Text = "TIEMPO POR PÁGINA · HOY"
        itm.BackToProjects.Hidden = true
        itm.RecentCard.Hidden = false
        itm.RecentTitle.Text="PROYECTOS RECIENTES"
        local maximum=1; for _,p in ipairs(pages) do maximum=math.max(maximum,cachedPages[p] or 0) end
        for i,p in ipairs(pages) do
            itm["PageRow"..i].Hidden=false; itm["PageName"..i].Text=labels[p]; itm["PageName"..i].Enabled=true
            itm["PageTime"..i].Text=clock(cachedPages[p] or 0); itm["PageBar"..i].Text=softBar((cachedPages[p] or 0)/maximum)
        end
        return
    end

    itm.RecentCard.Hidden = true
    if period=="month" and not selectedMonth then
        itm.BreakdownTitle.Text="MESES CON ACTIVIDAD"
        itm.BackToProjects.Hidden=true
        local months=availableMonths()
        local maximum=(months[1] and months[1].seconds) or 1
        for _,month in ipairs(months) do maximum=math.max(maximum,month.seconds) end
        for i=1,7 do
            local month=months[i]
            itm["PageRow"..i].Hidden=(month==nil)
            if month then
                currentRowMonths[i]=month; itm["PageName"..i].Enabled=true
                itm["PageName"..i].Text=month.label; itm["PageName"..i].ToolTip="Ver proyectos de "..month.label
                itm["PageTime"..i].Text=clock(month.seconds); itm["PageBar"..i].Text=softBar(month.seconds/maximum)
            end
        end
        return
    end
    local selected = nil
    if selectedProjectId then
        for _,project in ipairs(cachedProjects) do if project.id==selectedProjectId then selected=project; break end end
    end
    if selected then
        local periodLabel=period=="week" and "SEMANA" or (selectedMonth and selectedMonth.label:upper() or "MES")
        itm.BreakdownTitle.Text = selected.name:upper() .. " · " .. periodLabel
        itm.BackToProjects.Hidden = false
        itm.BackToProjects.Text = "‹ Proyectos"
        local maximum=1; for _,p in ipairs(pages) do maximum=math.max(maximum,selected.pages[p] or 0) end
        for i,p in ipairs(pages) do
            itm["PageRow"..i].Hidden=false; itm["PageName"..i].Text=labels[p]; itm["PageName"..i].Enabled=true
            itm["PageTime"..i].Text=clock(selected.pages[p] or 0); itm["PageBar"..i].Text=softBar((selected.pages[p] or 0)/maximum)
        end
        return
    end

    itm.BreakdownTitle.Text = "PROYECTOS · " .. (period=="week" and "SEMANA" or (selectedMonth and selectedMonth.label:upper() or "MES"))
    itm.BackToProjects.Hidden = not (period=="month" and selectedMonth~=nil)
    itm.BackToProjects.Text = "‹ Meses"
    local ordered={}; for _,project in ipairs(cachedProjects) do table.insert(ordered,project) end
    table.sort(ordered,function(a,b) return a.seconds>b.seconds end)
    local maximum=(ordered[1] and ordered[1].seconds) or 1
    for i=1,7 do
        local project=ordered[i]
        itm["PageRow"..i].Hidden=(project==nil)
        if project then
            currentRowProjects[i]=project; itm["PageName"..i].Enabled=true
            itm["PageName"..i].Text=project.name; itm["PageName"..i].ToolTip=project.name
            itm["PageTime"..i].Text=clock(project.seconds); itm["PageBar"..i].Text=softBar(project.seconds/maximum)
        end
    end
end

local function setPeriod(value, id)
    period=value
    selectedProjectId=nil
    selectedMonth=nil
    for _, key in ipairs({"ProjectPeriod","Day","Week","Month"}) do itm[key].Checked=(key==id) end
    refreshPeriodData()
    renderBreakdown()
    win:RecalcLayout()
end

function disp.On.Timeout(ev)
    if ev.who ~= timer.ID then return end
    ticks = ticks + 1
    if resetArmedUntil > 0 and os.time() > resetArmedUntil then
        resetArmedUntil = 0
        itm.ResetProject.Text = "Reiniciar proyecto"
    end
    if lastCounting then visualTotal = visualTotal + 0.25; visualProjectTotal = visualProjectTotal + 0.25 end
    -- Las consultas API/OS son relativamente pesadas. Se hacen cada 2 segundos;
    -- el reloj visual continúa por su cuenta cuatro veces por segundo.
    if ticks % 8 == 1 then
        local now = os.time()
        -- Resolve puede bloquear eventos de UI durante playback. Conservamos todo el
        -- intervalo anterior para que la reproducción no se pierda al reanudarse el timer.
        if lastCounting and lastState and now-lastTime > 0 and now-lastTime <= 43200 then appendSegment(lastState,lastTime,now) end
        local state, err = currentState()
        local idle, front = idleSeconds(), resolveFrontmost()
        -- Durante reproducción algunas consultas de la API devuelven nil. Si Resolve
        -- continúa al frente, el último proyecto/página conocidos siguen siendo válidos.
        if not state and lastState and front ~= false then state, err = lastState, nil end
        -- Estar dentro de Resolve cuenta también al reproducir, revisar o escuchar.
        -- El foco del sistema pausa al cambiar a cualquier otra aplicación.
        local active = front ~= false
        lastCounting = state ~= nil and active and not paused
        lastState, lastTime = state, now
        refreshPeriodData()
        visualProjectTotal = totalForProject(state and state.id)
        itm.Project.Text = state and (state.name .. "  ·  " .. labels[state.page]) or (err or "Resolve Timekeeper")
        local reason = paused and "Pausa manual" or (front == false and "Resolve no está al frente" or labels[state and state.page])
        itm.Status.Text = (lastCounting and "● Registrando" or "● En pausa") .. "  ·  " .. (reason or "En espera")
    end
    itm.Status.StyleSheet = lastCounting and "color:#e7ff58" or "color:#ff7b7b"
    itm.Clock.Text = clock(visualProjectTotal)
    itm.Total.Text = string.format("%d h %02d min", math.floor(visualTotal/3600), math.floor((visualTotal%3600)/60))
    if period=="project" then
        itm.ProjectCount.Text=string.format("%d día%s",#cachedDays,#cachedDays==1 and "" or "s")
    else
        itm.ProjectCount.Text = string.format("%d proyecto%s", #cachedProjects, #cachedProjects==1 and "" or "s")
    end
    renderBreakdown()
    local recentItems=period=="project" and cachedDays or cachedProjects
    local maxProject=1; for _,entry in ipairs(recentItems) do maxProject=math.max(maxProject,entry.seconds or 0) end
    for i=1,3 do
        local entry=recentItems[i]
        itm["ProjectRow"..i].Hidden=(entry==nil)
        if entry then
            local displayName=period=="project" and entry.label or entry.name
            itm["ProjectName"..i].Text=displayName
            itm["ProjectName"..i].ToolTip=displayName
            itm["ProjectTime"..i].Text=clock(entry.seconds)
            itm["ProjectBar"..i].Text=softBar(entry.seconds/maxProject)
        end
    end
    adjustWindowHeight(#recentItems, not itm.RecentCard.Hidden)
end

function win.On.ProjectPeriod.Clicked() setPeriod("project","ProjectPeriod") end
function win.On.Day.Clicked() setPeriod("day","Day") end
function win.On.Week.Clicked() setPeriod("week","Week") end
function win.On.Month.Clicked() setPeriod("month","Month") end
function win.On.BackToProjects.Clicked()
    if selectedProjectId then
        selectedProjectId=nil
    elseif period=="month" and selectedMonth then
        selectedMonth=nil
        refreshPeriodData()
    end
    renderBreakdown(); win:RecalcLayout()
end
for i=1,7 do
    local index=i
    win.On["PageName"..index].Clicked = function()
        local month=currentRowMonths[index]
        if month and period=="month" then
            selectedMonth=month; selectedProjectId=nil; refreshPeriodData(); renderBreakdown(); win:RecalcLayout(); return
        end
        local project=currentRowProjects[index]
        if project and period~="day" then selectedProjectId=project.id; renderBreakdown(); win:RecalcLayout() end
    end
end
function win.On.Pause.Clicked() paused=not paused; itm.Pause.Text=paused and "Reanudar" or "Pausar"; ticks=0 end
function win.On.ResetProject.Clicked()
    if not lastState then itm.ResetProject.Text="Abre un proyecto"; return end
    local now = os.time()
    if now > resetArmedUntil then
        resetArmedUntil = now + 6
        itm.ResetProject.Text = "Confirmar reinicio"
        return
    end
    local wasPaused = paused
    paused = true
    if resetProject(lastState.id) then
        visualProjectTotal = 0
        refreshPeriodData()
        itm.ResetProject.Text = "Tiempo reiniciado"
        lastTime = os.time() -- evita reinsertar el intervalo anterior al reinicio
    else
        itm.ResetProject.Text = "No se pudo reiniciar"
    end
    resetArmedUntil = 0
    paused = wasPaused
end
function win.On.ResolveTimekeeperWin.Close() timer:Stop(); disp:ExitLoop() end

win:RecalcLayout()
win:Show()
win:RecalcLayout()
disp.On.Timeout({who=timer.ID}) -- detecta y empieza a contar inmediatamente
timer:Start(); disp:RunLoop(); timer:Stop()

